跳到主要内容

Go 语言学习-“函数式编程”

这个标题起得不是很好,Golang 不是就是函数式的吗?为啥还取这个名称,这里主要是用于类别 Java 那种函数式编程(传递回调函数),与正常的编程方式

Go 的闭包

Go 语言支持匿名函数,可作为闭包。这个很像 JS 那个闭包

func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}

func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber()) // 1
fmt.Println(nextNumber()) // 2
fmt.Println(nextNumber()) // 3

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1()) // 1
fmt.Println(nextNumber1()) // 2

// 当然这个闭包也可以像 JS 那样玩
i := getSequence()()
fmt.Println(i) // 1
}

不过我个人理解,Go 中没有类型,无法使用类来包装(隔离)一些参数,所以可以使用闭包的方式缩小变量作用域,减少对全局变量的污染。

一个闭包相当于一个类的实例,函数体之外的变量相当于这个实例存储的变量。

所以某种程度上可以用闭包代替对象的操作

其实闭包其实使用还是很频繁的,例如下面这种匿名函数的场景,也是通过闭包的特性直接访问了 waitGroup

func main() {
var waitGroup sync.WaitGroup

waitGroup.Add(100)

for i := 0; i < 100; i++ {
go func() {
// do something....
waitGroup.Done()
}()
}

waitGroup.Wait()
}

因为有了闭包,函数可以直接访问到那些没有作为参数传入的变量。匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量本身。

快速使用 Go 的回调函数

// 声明一个函数类型
type cb func(int) int

func testCallBack(x int, f cb) {
f(x)
}

func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}

func main() {
// 传入同参方法
testCallBack(1, callBack)

// 或者直接使用匿名函数的方式
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}

接口型函数

接口型函数,指的是 用函数实现接口,这样在调用的时候就会非常简便,我称这种函数,为接口型函数,这种方式使用于只有一个函数的接口。

如下代码

// A Getter loads data for a key.
type Getter interface {
Get(key string) ([]byte, error)
}

// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)

// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {
return f(key)
}

接口型函数只能应用于接口内部只定义了一个方法的情况,例如接口 Getter 内部有且只有一个方法 Get。既然只有一个方法,为什么还要多此一举,封装为一个接口呢?定义参数的时候,直接用 GetterFunc 这个函数类型不就好了,让用户直接传入一个函数作为参数,不更简单吗?

所以呢,接口型函数的价值什么?

先说结论,接口型函数既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。

使用接口的方式可以保存很多上下文信息,而函数不行(闭包也可以,但是那个太麻烦了),最明显的例子就是 error 接口

type error interface {
Error() string
}

这样各个结构体实现了这个 Error() 方法就可以使用结构体内部的状态,而只使用回调函数是做不到的。

继续上面那个 Getter 接口的例子,它有多种方式调用该函数:

方式一:传递普通回调函数

这种场景可以直接使用回调函数就行了,没有必要使用到接口型函数

传递一个匿名回调函数

GetFromSource(GetterFunc(func(key string) ([]byte, error) {
return []byte(key), nil
}), "hello")

传递一个普通的函数

func test(key string) ([]byte, error) {
return []byte(key), nil
}

func main() {
GetFromSource(GetterFunc(test), "hello")
}

方式二:结构体方法

这种才是它正常的使用场景:

实现了 Getter 接口的结构体作为参数

type DB struct{ 
url string
// ... 很多属性
}

func (db *DB) Query(sql string, args ...string) string {
// ...
return "hello"
}

// 实现了 Getter 接口
func (db *DB) Get(key string) ([]byte, error) {
// ...
v := db.Query("SELECT NAME FROM TABLE WHEN NAME= ?", key)
return []byte(v), nil
}

func main() {
db := new(DB)
GetFromSource(db, "hello")
}

如上,就可以在这个 Get 方法里面使用到结构体里面的属性了。

这样,既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。

柯里化

这个柯里化也算是老面孔了,在 js 时就经常看到这个东西,实际上和上面闭包是一个概念的东西,都是在外层的函数保存状态,然后内层的函数复用这个状态

柯里化,英语:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

这个解释有一点抽象,就拿被做了无数次示例的 add 函数,来做一个简单的实现。

import "fmt"

// 普通的add函数
func add(x, y int) int {
return x + y
}

// Currying后
func curryingAdd(x int) func(int) int {
return func(y int) int {
return x + y
}
}

func main() {
fmt.Println(add(1, 2)) // 3
fmt.Println(curryingAdd(1)(2)) // 3
}

实际上就是把 add 函数的 x,y 两个参数变成了先用一个函数接收 x 然后返回一个函数去处理 y 参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

Currying 有哪些好处呢? ,这里直接使用 js 的例子了,反正概念是一样的

参数复用

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}

check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true

// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false

上面的示例是一个正则的校验,正常来说直接调用 check 函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数 reg 进行复用,这样别的地方就能够直接调用 hasNumber,hasLetter 等函数,让参数能够复用,调用起来也更方便。

使用任意数量的参数

这个例子和上面是一样的

func mkAdd(a int) func(...int) int {
return func(b... int) int {
for _, i := range b {
a += i
}
return a
}
}

func main() {
add2 := mkAdd(2)
add3 := mkAdd(3)
fmt.Println(add2(5,3), add3(6))
}

延迟计算

不断的柯里化,累积传入的参数,最后执行。

举个例子:

// Currying后
func curryingAdd() func(int) func() int {
arr := []int{}

// 这里只是记录参数
return func(y int) func() int {
arr = append(arr, y)

// 最后调用这个才计算结果
return func() int {
reslut := 0
for _, v := range arr {
reslut += v
}
return reslut
}
}
}

func main() {
mkadd := curryingAdd()
for i := 0; i < 100; i++ {
mkadd(i)
}
fmt.Println(mkadd(0)()) // 这里才计算结果
}

Reference

Golang必备技巧:接口型函数 详解JS函数柯里化 Go 接口型函数的使用场景